React + Material-UIで管理画面を作成してみた
最近、ReactとMaterial-UIを利用した管理画面を作成する機会がありましたので、Reactアプリの作成からMaterial-UIの導入までの手順をまとめてみました。本記事は前提としてJavaScriptやTypeScriptの知識があるとより分かりやすいと思います。
成果物
次のような管理画面を作成します。
環境
項目 | 内容 |
---|---|
OS | macOS Catalina 10.15.5(19F101) |
Node.js | 12.13.1 |
TypeScript | 3.7.2 |
React | 16.13.1 |
Material-UI | 4.11.0 |
Reactアプリを作成
Material-UIで管理画面を作るためのベースとなるReactアプリを作成します。
Create React App
Create React Appで新しいReactアプリを作成します。
npx create-react-app react-material-ui-sample --typescript
プロジェクトのディレクトリへ移動して実行します。
cd react-material-ui-sample npm start
ブラウザにReactアプリが表示されます。
ディレクトリ構成
ディレクトリはあまりネストさせすぎずシンプルな構造にしました。コンポーネントの分け方はAtomic Designを参考にしています。
src/ ├ components/ │ └ atoms/ # 原子(個々のパーツ) │ └ molecules/ # 分子(原子の集合体) │ └ organisms/ # 生体(分子の集合体) │ └ templates/ # テンプレート(ページの雛形) │ └ pages/ # ページ ├ App.tsx ├ index.css ├ index.tsx 〜〜〜〜〜〜〜〜〜〜〜〜〜
ページを作成
サンプルページとしてsrc/components/pages/
にHomePage.tsx
とProductPage.tsx
を作成します。
import React from "react"; const HomePage: React.FC = () => { return <>トップページ</>; }; export default HomePage;
import React from "react"; const ProductPage: React.FC = () => { return <>商品ページ</>; }; export default ProductPage;
ルーティング
指定のパスにアクセスした際、先ほど作成した2つのページが描写されるようにルーティングを実装してみます。
まずはルーティングを実装するためのライブラリをインストールします。
npm install --save react-router-dom npm install --save-dev @types/react-router-dom
App.tsxを編集してルーティングを実装します。
import React from "react"; import { BrowserRouter as Router, Route, Switch } from "react-router-dom"; import ProductPage from "./components/pages/ProductPage"; import HomePage from "./components/pages/HomePage"; const App: React.FC = () => { return ( <Router> <Switch> <Route path="/products" component={ProductPage} exact /> <Route path="/" component={HomePage} exact /> </Switch> </Router> ); }; export default App;
npm start
で「トップページ」と表示されれば成功です。またhttp://localhost:3000/products
へアクセスすると「商品ページ」と表示されます。
Material-UIを導入
Reactアプリの準備が出来たので、Material-UIを導入していきます。
Material-UIをインストール
Material-UIをインストールします。
npm install --save @material-ui/core @material-ui/icons
フォントを導入
Material-UIと相性の良いGoogle日本語フォントとフォントアイコンを導入しておきます。public/index.html
のヘッダーにCDNのURLを追加します。
<head> 〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜 <link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Roboto:300,400,500,700&display=swap" /> <link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Noto+Sans+JP&subset=japanese" /> <link rel="stylesheet" href="https://fonts.googleapis.com/icon?family=Material+Icons" /> 〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜 </head>
テンプレートを作成
src/components/templates/
にページのテンプレートを作成します。Material-UIが公開しているテンプレートを参考に実装してみます。
import React from "react"; import clsx from "clsx"; import { createMuiTheme } from "@material-ui/core/styles"; import * as colors from "@material-ui/core/colors"; import { makeStyles, createStyles, Theme } from "@material-ui/core/styles"; import { ThemeProvider } from "@material-ui/styles"; import CssBaseline from "@material-ui/core/CssBaseline"; import Drawer from "@material-ui/core/Drawer"; import Box from "@material-ui/core/Box"; import AppBar from "@material-ui/core/AppBar"; import Toolbar from "@material-ui/core/Toolbar"; import List from "@material-ui/core/List"; import Typography from "@material-ui/core/Typography"; import Divider from "@material-ui/core/Divider"; import Container from "@material-ui/core/Container"; import { Link } from "react-router-dom"; import MenuIcon from "@material-ui/icons/Menu"; import ChevronLeftIcon from "@material-ui/icons/ChevronLeft"; import IconButton from "@material-ui/core/IconButton"; import HomeIcon from "@material-ui/icons/Home"; import ShoppingCartIcon from "@material-ui/icons/ShoppingCart"; import ListItem from "@material-ui/core/ListItem"; import ListItemIcon from "@material-ui/core/ListItemIcon"; import ListItemText from "@material-ui/core/ListItemText"; const drawerWidth = 240; const theme = createMuiTheme({ typography: { fontFamily: [ "Noto Sans JP", "Lato", "游ゴシック Medium", "游ゴシック体", "Yu Gothic Medium", "YuGothic", "ヒラギノ角ゴ ProN", "Hiragino Kaku Gothic ProN", "メイリオ", "Meiryo", "MS Pゴシック", "MS PGothic", "sans-serif", ].join(","), }, palette: { primary: { main: colors.blue[800] }, // テーマの色 }, }); const useStyles = makeStyles((theme: Theme) => createStyles({ root: { display: "flex", }, toolbar: { paddingRight: 24, }, toolbarIcon: { display: "flex", alignItems: "center", justifyContent: "flex-end", padding: "0 8px", ...theme.mixins.toolbar, }, appBar: { zIndex: theme.zIndex.drawer + 1, transition: theme.transitions.create(["width", "margin"], { easing: theme.transitions.easing.sharp, duration: theme.transitions.duration.leavingScreen, }), }, appBarShift: { marginLeft: drawerWidth, width: `calc(100% - ${drawerWidth}px)`, transition: theme.transitions.create(["width", "margin"], { easing: theme.transitions.easing.sharp, duration: theme.transitions.duration.enteringScreen, }), }, menuButton: { marginRight: 36, }, menuButtonHidden: { display: "none", }, title: { flexGrow: 1, }, pageTitle: { marginBottom: theme.spacing(1), }, drawerPaper: { position: "relative", whiteSpace: "nowrap", width: drawerWidth, transition: theme.transitions.create("width", { easing: theme.transitions.easing.sharp, duration: theme.transitions.duration.enteringScreen, }), }, drawerPaperClose: { overflowX: "hidden", transition: theme.transitions.create("width", { easing: theme.transitions.easing.sharp, duration: theme.transitions.duration.leavingScreen, }), width: theme.spacing(7), [theme.breakpoints.up("sm")]: { width: theme.spacing(9), }, }, appBarSpacer: theme.mixins.toolbar, content: { flexGrow: 1, height: "100vh", overflow: "auto", }, container: { paddingTop: theme.spacing(4), paddingBottom: theme.spacing(4), }, paper: { padding: theme.spacing(2), display: "flex", overflow: "auto", flexDirection: "column", }, link: { textDecoration: "none", color: theme.palette.text.secondary, }, }) ); const Copyright = () => { return ( <Typography variant="body2" color="textSecondary" align="center"> {"Copyright © "} <Link color="inherit" to="/"> 管理画面 </Link>{" "} {new Date().getFullYear()} {"."} </Typography> ); }; export interface GenericTemplateProps { children: React.ReactNode; title: string; } const GenericTemplate: React.FC<GenericTemplateProps> = ({ children, title, }) => { const classes = useStyles(); const [open, setOpen] = React.useState(true); const handleDrawerOpen = () => { setOpen(true); }; const handleDrawerClose = () => { setOpen(false); }; return ( <ThemeProvider theme={theme}> <div className={classes.root}> <CssBaseline /> <AppBar position="absolute" className={clsx(classes.appBar, open && classes.appBarShift)} > <Toolbar className={classes.toolbar}> <IconButton edge="start" color="inherit" aria-label="open drawer" onClick={handleDrawerOpen} className={clsx( classes.menuButton, open && classes.menuButtonHidden )} > <MenuIcon /> </IconButton> <Typography component="h1" variant="h6" color="inherit" noWrap className={classes.title} > 管理画面 </Typography> </Toolbar> </AppBar> <Drawer variant="permanent" classes={{ paper: clsx(classes.drawerPaper, !open && classes.drawerPaperClose), }} open={open} > <div className={classes.toolbarIcon}> <IconButton onClick={handleDrawerClose}> <ChevronLeftIcon /> </IconButton> </div> <Divider /> <List> <Link to="/" className={classes.link}> <ListItem button> <ListItemIcon> <HomeIcon /> </ListItemIcon> <ListItemText primary="トップページ" /> </ListItem> </Link> <Link to="/products" className={classes.link}> <ListItem button> <ListItemIcon> <ShoppingCartIcon /> </ListItemIcon> <ListItemText primary="商品ページ" /> </ListItem> </Link> </List> </Drawer> <main className={classes.content}> <div className={classes.appBarSpacer} /> <Container maxWidth="lg" className={classes.container}> <Typography component="h2" variant="h5" color="inherit" noWrap className={classes.pageTitle} > {title} </Typography> {children} <Box pt={4}> <Copyright /> </Box> </Container> </main> </div> </ThemeProvider> ); }; export default GenericTemplate;
※再利用可能なパーツは別ファイルでコンポーネント化しておくと便利かもしれません。
テンプレートを使う
トップページと商品ページにテンプレートを適応します。
import React from "react"; import GenericTemplate from "../templates/GenericTemplate"; const HomePage: React.FC = () => { return ( <GenericTemplate title="トップページ"> <>トップページ内容</> </GenericTemplate> ); }; export default HomePage;
import React from "react"; import GenericTemplate from "../templates/GenericTemplate"; const ProductPage: React.FC = () => { return ( <GenericTemplate title="商品ページ"> <>商品ページ内容</> </GenericTemplate> ); }; export default ProductPage;
ブラウザでテンプレートが反映されていることを確認します。
コンポーネントを使う
Material-UIには様々なコンポーネントが用意されていて、コンポーネントを利用することで効率よく開発することが出来ます。例として商品ページにテーブルを実装してみます。
import React from "react"; import GenericTemplate from "../templates/GenericTemplate"; import { makeStyles } from "@material-ui/core/styles"; import Table from "@material-ui/core/Table"; import TableBody from "@material-ui/core/TableBody"; import TableCell from "@material-ui/core/TableCell"; import TableContainer from "@material-ui/core/TableContainer"; import TableHead from "@material-ui/core/TableHead"; import TableRow from "@material-ui/core/TableRow"; import Paper from "@material-ui/core/Paper"; const createData = ( name: string, category: string, weight: number, price: number ) => { return { name, category, weight, price }; }; const rows = [ createData("チョコレート", "お菓子", 100, 120), createData("ケーキ", "お菓子", 400, 480), createData("りんご", "フルーツ", 500, 360), createData("バナナ", "フルーツ", 200, 300), createData("みかん", "フルーツ", 250, 180), ]; const useStyles = makeStyles({ table: { minWidth: 650, }, }); const ProductPage: React.FC = () => { const classes = useStyles(); return ( <GenericTemplate title="商品ページ"> <TableContainer component={Paper}> <Table className={classes.table} aria-label="simple table"> <TableHead> <TableRow> <TableCell>商品名</TableCell> <TableCell align="right">カテゴリー</TableCell> <TableCell align="right">重量(g)</TableCell> <TableCell align="right">価格(円)</TableCell> </TableRow> </TableHead> <TableBody> {rows.map((row) => ( <TableRow key={row.name}> <TableCell component="th" scope="row"> {row.name} </TableCell> <TableCell align="right">{row.category}</TableCell> <TableCell align="right">{row.weight}</TableCell> <TableCell align="right">{row.price}</TableCell> </TableRow> ))} </TableBody> </Table> </TableContainer> </GenericTemplate> ); }; export default ProductPage;
商品ページにテーブルが作成されていることを確認します。
まとめ
ReactとMaterial-UIを利用することで、新規のアプリ開発が効率よく出来るようになりました。実際のWEBアプリでは認証機能やAPI疎通なども必要になりますので、それらも後々記事にできればと思います。